dtrace on macOS
2023-01-30 · 6 min read
setup: disable system integrity protection for dtrace
#
By default, macOS won't let you run dtrace
, even when run as root
. Fortunately we can can carve out a specific exception for dtrace
. To do that we'll have to boot into recovery mode:
- Turn off the computer.
- Hold down the power button for ~10 sec, until a screen pops up with two options.
- Select "Options" to boot into recovery mode.
- Ignore the initial screen and instead select "Terminal" from the top bar.
- Run
csrutil enable --without dtrace
. - Reboot normally.
Once you're booted back in, try running sudo dtruss bash -c echo foo
and you should see some output.
syscall counts of running process #
$ sudo dtrace -n 'syscall:::entry/pid == 1234/{ @syscalls[probefunc] = count(); }'
# .. collect data, then hit CTRL-C .. #
^C
chmod 3
close 3
fchown 3
rename 3
getuid 5
lstat64 6
# ..
fcntl 105
open_nocancel 313
fcntl_nocancel 322
fstat64 322
close_nocancel 391
read 465
write 480
stat64 685
kevent 1333
trace bytes read by all processes #
$ sudo dtrace -n "syscall::read:entry { @bytes[execname] = sum(arg2); }"
dtrace: description 'syscall::read:entry ' matched 1 probe
^C
sudo 2
plugin-container 10
karabiner_observer 4096
tmux 4096
karabiner_console_user_server 8192
karabiner_session_monitor 9216
cfprefsd 17665
firefox 32787
git 33225
karabiner_grabber 38912
Karabiner-DriverKit-VirtualHIDDe 51200
postgres 57397
locationd 57680
sharingd 57680
Obsidian Helper (Renderer) 399360
mds 1326816
fseventsd 1777432
alacritty 4194560
mdworker_shared 5242880
trace spawned processes #
$ sudo dtrace -qn "syscall::posix_spawn:entry { printf(\"%Y %s\n\", walltimestamp, copyinstr(arg1)); }"
^[2023 Jan 30 13:29:50 /usr/bin/git
2023 Jan 30 13:29:50 /usr/bin/git
2023 Jan 30 13:29:52 /usr/libexec/xpcproxy
2023 Jan 30 13:29:52 /opt/homebrew/opt/postgresql@14/bin/postgres
2023 Jan 30 13:29:55 /usr/libexec/xpcproxy
2023 Jan 30 13:29:55 /System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Versions/A/Support/mdworker_shared
2023 Jan 30 13:30:02 /usr/libexec/xpcproxy
2023 Jan 30 13:30:02 /opt/homebrew/opt/postgresql@14/bin/postgres
2023 Jan 30 13:30:05 /usr/libexec/xpcproxy
2023 Jan 30 13:30:05 /usr/sbin/newsyslog
2023 Jan 30 13:30:12 /usr/libexec/xpcproxy
2023 Jan 30 13:30:12 /opt/homebrew/opt/postgresql@14/bin/postgres
Find longest syscalls #
$ sudo dtrace -qn \
'syscall:::entry { self->ts = timestamp; } syscall:::return/self->ts/ { printf("%d µs, %s\n", (timestamp - self->ts) / 1000, probefunc); self->ts = 0; }' \
-c 'bash -c "python3 -c \"print(123)\""' \
| sort -h
..
42 µs, ioctl
44 µs, open_nocancel
44 µs, shared_region_check_np
351 µs, execve
943 µs, ioctl
1279 µs, workq_kernreturn
1348 µs, psynch_cvwait
some oneliners #
# Files opened by process. Very useful when reversing:
dtrace -n 'syscall::open*:entry { printf("%s %s", execname, copyinstr(arg0)); }'
# Syscall count by program:
dtrace -n 'syscall:::entry { @num[execname] = count(); }'
# Syscall count by syscall:
dtrace -n 'syscall:::entry { @num[probefunc] = count(); }'
# Syscall count by PID:
dtrace -n 'syscall:::entry { @num[pid,execname] = count(); }'
# Read bytes by process:
dtrace -n 'syscall::read:entry { @bytes[execname] = sum(arg0); }'
# Periodic breakdown of written bytes by process:
dtrace -n 'syscall::write:* { @bytes[execname] = sum(arg0); } tick-5sec { printa(@bytes); trunc(@bytes); }'
# Read size distribution by process:
dtrace -n 'syscall::read:entry { @dist[execname] = quantize(arg0); }'
# Write size distribution by process:
dtrace -n 'syscall::write:entry { @dist[execname] = quantize(arg0); }'
# Disk size (in blocks) by process:
dtrace -n 'io:::start { printf("%d %s %d", pid, execname, args[0]->b_bcount); }'
# Pages paged in by process:
dtrace -n 'vminfo::vm_fault_enter:pgin { @pg[execname] = sum(arg0); }'
# Minor faults by process:
dtrace -n 'vminfo:::as_fault { @mem[execname] = sum(arg0); }'
# Interrupts by CPU:
dtrace -n 'sdt:::interrupt-start { @num[cpu] = count(); }'
# New processes with arguments and time:
dtrace -qn 'syscall::exec*:return { printf("%Y %s\n",walltimestamp,curpsinfo->pr_psargs); }'
# Successful signal details:
dtrace -n 'proc:::signal-send /pid/ { printf("%s -%d %d",execname,args[2],args[1]->pr_pid); }'
# System call breakdown for the process with PID 31337:
dtrace -n syscall:::entry'/pid == 31337/{ @syscalls[probefunc] = count(); }'
# Tracking memory page faults for process:
dtrace -n 'vminfo:::as_fault { @mem[execname] = sum(arg0); }'
# iOS Intruments-like malloc size distribution plot:
dtrace -n 'pid$target::malloc:entry { @ = quantize(arg0); }' -p PID
# Memory allocation via malloc by stack trace and requested size:
dtrace -n 'pid$target::malloc:entry { @[ustack()] = sum(arg0); }' -p PID
# Rate of disk I/O:
dtrace -n 'io:::start { @io = count(); } tick-1sec { printa("Disk I/Os per second: %@d\n", @io); trunc(@io); }'
trace spawned processes w/ full arguments #
// file: /usr/local/bin/traceexec.d
#!/usr/sbin/dtrace -C -s
#pragma D option quiet
dtrace:::BEGIN
{
printf("PID\tPPID\tTIME\tPATH\tARGV\n")
}
// remember that probes are executed _in the order they appear in the file_
// and that probe-local variables (this->XXX) are shared across activated
// probes.
// Since `arg0` and `argv[idx]` are in user-space and probes are run in
// kernel-space, we have to first `copyin[str](..)` the user-space strings
// before we can process them.
syscall::execve:entry
/pid == $target || progenyof($target)/
{
this->path = copyinstr(arg0);
this->argv = args[1];
}
syscall::posix_spawn:entry
/pid == $target || progenyof($target)/
{
this->path = copyinstr(arg1);
this->argv = args[3];
}
#define SAFE_ARGC(idx) ((this->argc != (idx) || this->argv[(idx)] == NULL) ? this->argc : (this->argc + 1))
#define SAFE_ARGV(idx) ((this->argc > (idx)) ? copyinstr((user_addr_t) this->argv[(idx)]) : "")
syscall::execve:entry,
syscall::posix_spawn:entry
/pid == $target || progenyof($target)/
{
this->pid=pid;
this->ppid=ppid;
// DTrace doesn't support loops, so we have to manually unroll the argc
// and argv processing.
this->argc = 0;
this->argc = SAFE_ARGC(0);
this->argc = SAFE_ARGC(1);
this->argc = SAFE_ARGC(2);
this->argc = SAFE_ARGC(3);
this->argc = SAFE_ARGC(4);
this->argc = SAFE_ARGC(5);
this->argc = SAFE_ARGC(6);
this->argc = SAFE_ARGC(7);
this->argc = SAFE_ARGC(8);
this->argc = SAFE_ARGC(9);
printf("%d\t", pid);
printf("%d\t", ppid);
printf("%Y\t", walltimestamp);
printf("%s\t", this->path);
printf("%s ", SAFE_ARGV(1));
printf("%s ", SAFE_ARGV(2));
printf("%s ", SAFE_ARGV(3));
printf("%s ", SAFE_ARGV(4));
printf("%s ", SAFE_ARGV(5));
printf("%s ", SAFE_ARGV(6));
printf("%s ", SAFE_ARGV(7));
printf("%s ", SAFE_ARGV(8));
printf("%s ", SAFE_ARGV(9));
printf("\n");
}
#undef SAFE_ARGC
#undef SAFE_ARGV